/* Copyright 2021 Aristocratos (jakob@qvantnet.com)

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

indent = tab
tab-size = 4
*/

#pragma warning (disable : 4455)

#include <iostream>
#include <ranges>
#include <vector>
#include <thread>
#include <mutex>

#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#define VC_EXTRALEAN
#include <windows.h>

#include <btop_input.hpp>
#include <btop_tools.hpp>
#include <btop_config.hpp>
#include <btop_shared.hpp>
#include <btop_menu.hpp>
#include <btop_draw.hpp>
#include <signal.h>

using std::cin, std::vector, std::string_literals::operator""s;
using namespace Tools;
namespace rng = std::ranges;

namespace Input {

	//* Map for translating key codes to readable values
	const unordered_flat_map<char, string> Key_escapes = {
		{VK_ESCAPE,	"escape"},
		{VK_RETURN,	"enter"},
		{VK_SPACE,	"space"},
		{VK_BACK,	"backspace"},
		{VK_UP,		"up"},
		{VK_DOWN,	"down"},
		{VK_LEFT,	"left"},
		{VK_RIGHT,	"right"},
		{VK_INSERT,	"insert"},
		{VK_DELETE,	"delete"},
		{VK_HOME,	"home"},
		{VK_END,	"end"},
		{VK_PRIOR,	"page_up"},
		{VK_NEXT,	"page_down"},
		{VK_TAB,	"tab"},
		{VK_F1,		"f1"},
		{VK_F2,		"f2"},
		{VK_F3,		"f3"},
		{VK_F4,		"f4"},
		{VK_F5,		"f5"},
		{VK_F6,		"f6"},
		{VK_F7,		"f7"},
		{VK_F8,		"f8"},
		{VK_F9,		"f9"},
		{VK_F10,	"f10"},
		{VK_F11,	"f11"},
		{VK_F12,	"f12"}
	};

	std::atomic<bool> interrupt (false);
	std::atomic<bool> polling (false);
	array<int, 2> mouse_pos;
	unordered_flat_map<string, Mouse_loc> mouse_mappings;

	deque<string> history(50, "");
	int last_mouse_button = 0;
	string old_filter;

	bool poll(int timeout) {
		HANDLE handleIn = GetStdHandle(STD_INPUT_HANDLE);
		DWORD waiting;
		
		while (timeout > 0) {
			if (interrupt) {
				interrupt = false;
				return false;
			}
			if (not GetNumberOfConsoleInputEvents(handleIn, &waiting))
				throw std::runtime_error("Failed getting number of input events!");
			if (waiting > 0) return true;
			sleep_ms(timeout < 10 ? timeout : 10);
			timeout -= 10;
		}
		return false;
	}

	string get() {
		HANDLE handleIn = GetStdHandle(STD_INPUT_HANDLE);
		DWORD cNumRead;
		INPUT_RECORD iRec;
		string key;

		if (not ReadConsoleInput(handleIn, &iRec, 1, &cNumRead))
			throw std::runtime_error("Failed reading input!");

		if (cNumRead > 0) {
			if (iRec.EventType == WINDOW_BUFFER_SIZE_EVENT) {
				Input::interrupt = true;
				return "";
			}
			else if (iRec.EventType == KEY_EVENT and iRec.Event.KeyEvent.bKeyDown) {
				if (Key_escapes.contains(iRec.Event.KeyEvent.wVirtualKeyCode)) {
					key = Key_escapes.at(iRec.Event.KeyEvent.wVirtualKeyCode);

					if (key == "tab" and (iRec.Event.KeyEvent.dwControlKeyState & SHIFT_PRESSED))
						key = "shift_tab";
				}
				else if (iRec.Event.KeyEvent.bKeyDown) {
					char c = iRec.Event.KeyEvent.uChar.UnicodeChar;

					if (c > 32 and c < 126)
						key = c;
					else
						return "";
					
					history.push_back(key);
					history.pop_front();
				}
			}
			else if (iRec.EventType == MOUSE_EVENT) {
				
				if (iRec.Event.MouseEvent.dwButtonState == FROM_LEFT_1ST_BUTTON_PRESSED
				or (iRec.Event.MouseEvent.dwEventFlags == DOUBLE_CLICK and last_mouse_button == FROM_LEFT_1ST_BUTTON_PRESSED)) {
					key = "mouse_click";
					last_mouse_button = FROM_LEFT_1ST_BUTTON_PRESSED;
				}
				else if (iRec.Event.MouseEvent.dwEventFlags == MOUSE_WHEELED) {
					key = iRec.Event.MouseEvent.dwButtonState >> 24 == 0 ? "mouse_scroll_up" : "mouse_scroll_down";
				}
				else if (iRec.Event.MouseEvent.dwButtonState == RIGHTMOST_BUTTON_PRESSED 
				or (iRec.Event.MouseEvent.dwEventFlags == DOUBLE_CLICK and last_mouse_button == RIGHTMOST_BUTTON_PRESSED)) {
					last_mouse_button = RIGHTMOST_BUTTON_PRESSED;
					return "escape";
				}
				else {
					return "";
				}

				//Logger::debug(mouse_event);
				
				if (Config::getB("proc_filtering")) {
					if (key == "mouse_click") return key;
					else return "";
				}

				//? Get column and line position of mouse and check for any actions mapped to current position
				if (not key.empty()) {
					mouse_pos[0] = iRec.Event.MouseEvent.dwMousePosition.X + 1;
					mouse_pos[1] = iRec.Event.MouseEvent.dwMousePosition.Y + 1;

					if (key == "mouse_click") {
						const auto& [col, line] = mouse_pos;

						for (const auto& [mapped_key, pos] : (Menu::active ? Menu::mouse_mappings : mouse_mappings)) {
							if (col >= pos.col and col < pos.col + pos.width and line >= pos.line and line < pos.line + pos.height) {
								key = mapped_key;
								break;
							}
						}
					}
				}

			}
		}
		return key;
	}

	void process(const string& key) {
		if (key.empty()) return;
		try {
			auto& filtering = Config::getB("proc_filtering");
			auto& vim_keys = Config::getB("vim_keys");
			auto help_key = (vim_keys ? "H" : "h");
			auto kill_key = (vim_keys ? "K" : "k");
			//? Global input actions
			if (not filtering) {
				bool keep_going = false;
				if (str_to_lower(key) == "q") {
					clean_quit(0);
				}
				else if (is_in(key, "escape", "m")) {
					Menu::show(Menu::Menus::Main);
					return;
				}
				else if (is_in(key, "f1", help_key)) {
					Menu::show(Menu::Menus::Help);
					return;
				}
				else if (is_in(key, "f2", "o")) {
					Menu::show(Menu::Menus::Options);
					return;
				}
				else if (is_in(key, "1", "2", "3", "4")) {
					atomic_wait(Runner::active);
					Config::current_preset = -1;
					static const array<string, 4> boxes = {"cpu", "mem", "net", "proc"};
					Config::toggle_box(boxes.at(std::stoi(key) - 1));
					Draw::calcSizes();
					Runner::run("all", false, true);
					return;
				}
				else if (is_in(key, "p", "P") and Config::preset_list.size() > 1) {
					if (key == "p") {
						if (++Config::current_preset >= (int)Config::preset_list.size()) Config::current_preset = 0;
					}
					else {
						if (--Config::current_preset < 0) Config::current_preset = Config::preset_list.size() - 1;
					}
					atomic_wait(Runner::active);
					Config::apply_preset(Config::preset_list.at(Config::current_preset));
					Draw::calcSizes();
					Runner::run("all", false, true);
					return;
				}
				else
					keep_going = true;

				if (not keep_going) return;
			}

			//? Input actions for proc box
			if (Proc::shown) {
				bool keep_going = false;
				bool no_update = true;
				bool redraw = true;
				if (filtering) {
					if (key == "enter" or key == "down") {
						Config::set("proc_filter", Proc::filter.text);
						Config::set("proc_filtering", false);
                        old_filter.clear();
                        if(key == "down"){
                            process("down");
                            return;
                        }
                    }
					else if (key == "escape" or key == "mouse_click") {
						Config::set("proc_filter", old_filter);
						Config::set("proc_filtering", false);
						old_filter.clear();
					}
					else if (Proc::filter.command(key)) {
						if (Config::getS("proc_filter") != Proc::filter.text)
							Config::set("proc_filter", Proc::filter.text);
					}
					else
						return;
				}
				else if (key == "left" or (vim_keys and key == "h")) {
					if (Config::getB("proc_services")) {
						int cur_i = v_index(Proc::sort_vector_service, Config::getS("services_sorting"));
						if (--cur_i < 0)
							cur_i = Proc::sort_vector_service.size() - 1;
						Config::set("services_sorting", Proc::sort_vector_service.at(cur_i));
					}
					else {
						int cur_i = v_index(Proc::sort_vector, Config::getS("proc_sorting"));
						if (--cur_i < 0)
							cur_i = Proc::sort_vector.size() - 1;
						Config::set("proc_sorting", Proc::sort_vector.at(cur_i));
					}
				}
				else if (key == "right" or (vim_keys and key == "l")) {
					if (Config::getB("proc_services")) {
						int cur_i = v_index(Proc::sort_vector_service, Config::getS("services_sorting"));
						if (std::cmp_greater(++cur_i, Proc::sort_vector_service.size() - 1))
							cur_i = 0;
						Config::set("services_sorting", Proc::sort_vector_service.at(cur_i));
					}
					else {
						int cur_i = v_index(Proc::sort_vector, Config::getS("proc_sorting"));
						if (std::cmp_greater(++cur_i, Proc::sort_vector.size() - 1))
							cur_i = 0;
						Config::set("proc_sorting", Proc::sort_vector.at(cur_i));
					}
				}
				else if (is_in(key, "f", "/")) {
					Config::flip("proc_filtering");
					Proc::filter = { Config::getS("proc_filter") };
					old_filter = Proc::filter.text;
				}
				else if (key == "e" and not Config::getB("proc_services")) {
					Config::flip("proc_tree");
					no_update = false;
				}

				else if (key == "r")
					Config::flip("proc_reversed");

				else if (key == "c")
					Config::flip("proc_per_core");

				else if (key == "delete" and not Config::getS("proc_filter").empty())
					Config::set("proc_filter", ""s);

				else if (key.starts_with("mouse_")) {
					redraw = false;
					const auto& [col, line] = mouse_pos;
					const int y = (Config::getB("show_detailed") ? Proc::y + 8 : Proc::y);
					const int height = (Config::getB("show_detailed") ? Proc::height - 8 : Proc::height);
					if (col >= Proc::x + 1 and col < Proc::x + Proc::width and line >= y + 1 and line < y + height - 1) {
						if (key == "mouse_click") {
							if (col < Proc::x + Proc::width - 2) {
								const auto& current_selection = Config::getI("proc_selected");
								if (current_selection == line - y - 1) {
									redraw = true;
									if (not Config::getB("proc_services") and Config::getB("proc_tree")) {
										const int x_pos = col - Proc::x;
										const int offset = Config::getI("selected_depth") * 3;
										if (x_pos > offset and x_pos < 4 + offset) {
											process("space");
											return;
										}
									}
									process("enter");
									return;
								}
								else if (current_selection == 0 or line - y - 1 == 0)
									redraw = true;
								Config::set("proc_selected", line - y - 1);
							}
							else if (line == y + 1) {
								if (Proc::selection("page_up") == -1) return;
							}
							else if (line == y + height - 2) {
								if (Proc::selection("page_down") == -1) return;
							}
							else if (Proc::selection("mousey" + to_string(line - y - 2)) == -1)
								return;
						}
						else
							goto proc_mouse_scroll;
					}
					else if (key == "mouse_click" and Config::getI("proc_selected") > 0) {
						Config::set("proc_selected", 0);
						redraw = true;
					}
					else
						keep_going = true;
				}
				else if (key == "enter") {
					if (Config::getI("proc_selected") == 0 and not Config::getB("show_detailed")) {
						return;
					}
					else if (Config::getI("proc_selected") > 0 and (Config::getI("detailed_pid") != Config::getI("selected_pid") or Config::getS("detailed_name") != Config::getS("selected_name"))) {
						Config::set("detailed_pid", Config::getI("selected_pid"));
						Config::set("detailed_name", Config::getS("selected_name"));
						Config::set("proc_last_selected", Config::getI("proc_selected"));
						Config::set("proc_selected", 0);
						Config::set("show_detailed", true);
					}
					else if (Config::getB("show_detailed")) {
						if (Config::getI("proc_last_selected") > 0) Config::set("proc_selected", Config::getI("proc_last_selected"));
						Config::set("proc_last_selected", 0);
						Config::set("detailed_pid", 0);
						Config::set("detailed_name", ""s);
						Config::set("show_detailed", false);
					}
				}
				else if (is_in(key, "+", "-", "space") and not Config::getB("proc_services") and Config::getB("proc_tree") and Config::getI("proc_selected") > 0) {
					atomic_wait(Runner::active);
					auto& pid = Config::getI("selected_pid");
					if (key == "+" or key == "space") Proc::expand = pid;
					if (key == "-" or key == "space") Proc::collapse = pid;
					no_update = false;
				}
				else if (is_in(key, "t", kill_key) and (Config::getB("show_detailed") or Config::getI("selected_pid") > 0 or not Config::getS("selected_name").empty())) {
					atomic_wait(Runner::active);
					if (not Config::getB("proc_services") and Config::getB("show_detailed") and Config::getI("proc_selected") == 0 and Proc::detailed.status == "Stopped") return;
					Menu::show(Menu::Menus::SignalSend);
					return;
				}
				else if (key == "u" and Config::getB("proc_services") and Config::getB("show_detailed")) {
					atomic_wait(Runner::active);
					if (not Proc::detailed.can_pause or Proc::detailed.status == "Stopped") return;
					Menu::show(Menu::Menus::SignalPause);
					return;
				}
				else if (key == "S" and Config::getB("proc_services") and Config::getB("show_detailed")) {
					Menu::show(Menu::Menus::SignalConfig);
					return;
				}
				else if (key == "s") {
					atomic_wait(Runner::active);
					Config::flip("proc_services");
					Config::set("proc_selected", 0);
					Config::set("proc_last_selected", 0);
					Config::set("detailed_pid", 0);
					Config::set("detailed_name", ""s);
					Config::set("show_detailed", false);
					no_update = false;
				}
				else if (is_in(key, "up", "down", "page_up", "page_down", "home", "end") or (vim_keys and is_in(key, "j", "k", "g", "G"))) {
					proc_mouse_scroll:
					redraw = false;
					auto old_selected = Config::getI("proc_selected");
					auto new_selected = Proc::selection(key);
					if (new_selected == -1)
						return;
					else if (old_selected != new_selected and (old_selected == 0 or new_selected == 0))
						redraw = true;
				}
				else keep_going = true;

				if (not keep_going) {
					Runner::run("proc", no_update, redraw);
					return;
				}
			}

			//? Input actions for cpu box
			if (Cpu::shown) {
				bool keep_going = false;
				bool no_update = true;
				bool redraw = true;
				static uint64_t last_press = 0;

				if (key == "+" and Config::getI("update_ms") <= 86399900) {
					int add = (Config::getI("update_ms") <= 86399000 and last_press >= time_ms() - 200
						and rng::all_of(Input::history, [](const auto& str){ return str == "+"; })
						? 1000 : 100);
					Config::set("update_ms", Config::getI("update_ms") + add);
					last_press = time_ms();
					redraw = true;
				}
				else if (key == "-" and Config::getI("update_ms") >= 200) {
					int sub = (Config::getI("update_ms") >= 2000 and last_press >= time_ms() - 200
						and rng::all_of(Input::history, [](const auto& str){ return str == "-"; })
						? 1000 : 100);
					Config::set("update_ms", Config::getI("update_ms") - sub);
					last_press = time_ms();
					redraw = true;
				}
				else keep_going = true;

				if (not keep_going) {
					Runner::run("cpu", no_update, redraw);
					return;
				}
			}

			//? Input actions for mem box
			if (Mem::shown) {
				bool keep_going = false;
				bool no_update = true;
				bool redraw = true;

				if (key == "i") {
					Config::flip("io_mode");
				}
				else if (key == "d") {
					Config::flip("show_disks");
					no_update = false;
					Draw::calcSizes();
				}
				else keep_going = true;

				if (not keep_going) {
					Runner::run("mem", no_update, redraw);
					return;
				}
			}

			//? Input actions for net box
			if (Net::shown) {
				bool keep_going = false;
				bool no_update = true;
				bool redraw = true;

				if (is_in(key, "b", "n")) {
					atomic_wait(Runner::active);
					int c_index = v_index(Net::interfaces, Net::selected_iface);
					if (c_index != (int)Net::interfaces.size()) {
						if (key == "b") {
							if (--c_index < 0) c_index = Net::interfaces.size() - 1;
						}
						else if (key == "n") {
							if (++c_index == (int)Net::interfaces.size()) c_index = 0;
						}
						Net::selected_iface = Net::interfaces.at(c_index);
						Net::rescale = true;
					}
				}
				else if (key == "y") {
					Config::flip("net_sync");
					Net::rescale = true;
				}
				else if (key == "a") {
					Config::flip("net_auto");
					Net::rescale = true;
				}
				else if (key == "z") {
					atomic_wait(Runner::active);
					auto& ndev = Net::current_net.at(Net::selected_iface);
					if (ndev.stat.at("download").offset + ndev.stat.at("upload").offset > 0) {
						ndev.stat.at("download").offset = 0;
						ndev.stat.at("upload").offset = 0;
					}
					else {
						ndev.stat.at("download").offset = ndev.stat.at("download").last + ndev.stat.at("download").rollover;
						ndev.stat.at("upload").offset = ndev.stat.at("upload").last + ndev.stat.at("upload").rollover;
					}
					no_update = false;
				}
				else keep_going = true;

				if (not keep_going) {
					Runner::run("net", no_update, redraw);
					return;
				}
			}
		}


		catch (const std::exception& e) {
			throw std::runtime_error("Input::process(\"" + key + "\") : " + (string)e.what());
		}
	}

}